local json = require "cjson"
local config = require "config"

---! 映射表
local load_map = {}     ---! 正在加载的玩家对象
local user_map = {}     ---! 已经加载的玩家对象
local temp_map = {}     ---! 临时缓存的玩家对象
local user_steam_map = {} ---!以steam_id为key 保存的玩家对象

---! 事件表
local enter_world_map = {}  ---! 进入游戏时的处理事件
local leave_world_map = {}  ---! 离开游戏时的处理事件

local user_level_init = 1 ---! 玩家初始等级
local user_exp_init = 0 ---! 玩家初始经验

local function _load_user(user_id)
    ---! 临时对象
    local user_ob = temp_map[user_id]
    if user_ob then
        return user_ob
    end

    local steam_id = STEAM_D:get_steam_id_by_user_id(user_id)

    ---! 创建对象
    local user_ob = USER_OB:create()
    user_ob:set_id(user_id)
    user_ob:set_steam_id(steam_id)
    user_ob:set_level(user_level_init)
    user_ob:set_exp(user_exp_init)
    

    ---! 加载数据
    local ok = user_ob:load()
    if not ok then
        return
    end

    ---! 记录对象
    temp_map[user_id] = user_ob
    user_steam_map[steam_id] = user_ob

    ---! 返回对象
    return user_ob
end

local function _check_online_user(user_id, user_ob, check_time)
    if user_ob:is_robot() then
        ---! 机器人忽略查岗
        return
    end

    local save_time = user_ob:query_temp("save_time")
    if not save_time then
        ---! 记录保存时间
        user_ob:set_temp("save_time", check_time)
        return
    end

    local cmd_time = user_ob:query_temp("command_time")
    if not cmd_time or check_time - cmd_time > 300 then
        ---! 断开玩家连接
        USER_D:disconnect(user_ob, 3)

        ---! 安排玩家下线
        USER_D:leave_world(user_ob)
        return
    end

    if check_time - save_time > 180 then
        ---! 保存玩家数据
        user_ob:save()
        user_ob:set_temp("save_time", check_time)
    end
end

local function _check_offline_user(user_id, user_ob, check_time)
    if user_ob:is_robot() then
        ---! 机器人忽略查岗
        return
    end

    if USER_D:is_loading(user_id) then
        return
    end

    if USER_D:find_user(user_id) then
        return
    end

    local cached_time = user_ob:query_temp("cached_time")
    if cached_time and check_time < cached_time + 600 then
        return
    end

    temp_map[user_id] = nil
end

---! 定时保存用户数据
local function _check_user()
    ---! 获取当前时间
    local now_time = os.time()

    ---! 遍历在线玩家
    for user_id, user_ob in pairs(user_map) do
        local ok = pcall(_check_online_user, user_id, user_ob, now_time)
        if not ok then
            spdlog.error("user", string.format("save user data failed, user = %s, data = %s", user_ob:get_id(), json.encode(user_ob:query_entire_dbase())))
        end
    end

    ---! 遍历缓存玩家
    for user_id, user_ob in pairs(temp_map) do
        local ok = pcall(_check_offline_user, user_id, user_ob, now_time)
        if not ok then
            spdlog.error("user", string.format("clean cached data failed, user = %s, data = %s", user_ob:get_id(), json.encode(user_ob:query_entire_dbase())))
        end
    end
end

-------------------------------------------------------------------------------
---! 对外接口
-------------------------------------------------------------------------------
USER_D = {}

function USER_D:find_user(user_id)
    if not user_id then
        return
    end

    local user_ob = user_map[tonumber(user_id)]
    if user_ob then
        return user_ob
    end
end

---! 查找临时对象
function USER_D:find_temp_user(user_id)
    local user_ob = temp_map[user_id]
    if user_ob then
        return user_ob
    end
end

---! 判断是否正在加载
function USER_D:is_loading(user_id)
    if load_map[user_id] then
        return true
    end
    return false
end

---! 通过steam_id获取离线玩家数据
---! 如果数据库不存在,则返回的对象 id为0
function USER_D:load_user_by_steam_id(steam_id)

    local user_ob = user_steam_map[steam_id]
    if user_ob then
        return user_ob
    end
    local now = os.clock()
    local user_ob = USER_OB:create()
    user_ob:set_id(0)
    user_ob:set_steam_id(steam_id)
    local ok = user_ob:load()
    spdlog.debug("userd", string.format("user [%s] load cost time = %s", steam_id, os.clock() - now))
    if not ok then
        return
    end
    user_steam_map[steam_id] = user_ob
    STEAM_D:set_steam_id_by_user_id(user_ob:get_id(),steam_id)

    return user_ob
end

---! 加载对象
function USER_D:load_user(user_id)
    ---! 获取当前时间
    local now = os.clock()
    ---! 设置加载状态
    load_map[user_id] = now
    ---! 开始加载对象
    spdlog.debug("userd", string.format("user [%s] start load ...", user_id))
    local user_ob = _load_user(user_id)
    spdlog.debug("userd", string.format("user [%s] load cost time = %s", user_id, os.clock() - now))
    ---! 解除加载状态
    load_map[user_id] = nil
    return user_ob
end

---! 初始对象
function USER_D:init_user(user_ob)
    if user_ob:get_create_time() then
        return
    end

    spdlog.info("user", string.format("init user [%s] data", user_ob:get_id()))

    ---! 设置角色创建时间
    user_ob:set_create_time()

    ---! 保存玩家数据
    user_ob:save()

    ---! 记录创建日志
    ----todo:
end

---! 进入游戏
function USER_D:enter_world(user_ob)
    local user_id = user_ob:get_id()
    user_map[user_id], temp_map[user_id] = user_ob, nil
    spdlog.info("userd", string.format("user [%s] enter world succ.", user_id))

    ---! 触发进入游戏相关事件
    for _, func in pairs(enter_world_map) do
        xpcall(func, error_traceback, user_ob)
    end

    ---! 记录当前玩家进入时间
    user_ob:set_temp("start_online_time", os.time())
end

---! 离开游戏
function USER_D:leave_world(user_ob)
    local clientAddr = user_ob:query_temp("clientAddr")
    local clientId = user_ob:query_temp("clientId")
    if clientAddr and clientId then
        return
    end

    ---! 清理临时数据
    user_ob:delete_temp("save_time")
    user_ob:delete_temp("command_time")

    ---! 触发相关事件
    for _, func in pairs(leave_world_map) do
        xpcall(func, error_traceback, user_ob)
    end

    ---! 优先保存一下
    user_ob:save()

    ---! 缓存当前对象
    local user_id = user_ob:get_id()
    user_map[user_id], temp_map[user_id] = nil, user_ob
    spdlog.info("userd", string.format("user [%s:%s] leave world succ.", user_id,user_ob:get_steam_id()))

    ---! 记录缓存时间
    user_ob:set_temp("cached_time", os.time())

    ---! 记录在线时长
    local online_time = user_ob:set_temp("last_online_time", user_ob:get_online_time())

    ---! 记录登出日志
    ----todo:
end

---! 重连游戏
function USER_D:reconnect(user_ob)
    local user_id = user_ob:get_id()
    spdlog.info("userd", string.format("user [%s:%s] reconnect world ...", user_id,user_ob:get_steam_id()))

    --退出匹配
    MATCH_D:quit_match(user_ob)
    --退出战斗
    COMBAT_D:quit_combat(user_ob)
    --退出队伍
    TEAM_D:quit_team(user_ob)

    local func = user_ob:query_temp("reconnect_callback")
    if func then
        xpcall(func, error_traceback, user_ob)
    end
end

---! 断开游戏
function USER_D:disconnect(user_ob, reason)
    --[[
    local clientAddr = user_ob:query_temp("clientAddr")
    if not clientAddr then
        return
    end

    local clientId = user_ob:query_temp("clientId")
    if not clientId then
        return
    end
    --]]

    spdlog.info("userd", string.format("user [%s:%s] disconnect world ...", user_ob:get_id(),user_ob:get_steam_id()))
    --[[
    local func = user_ob:query_temp("disconnect_callback")
    if func then
        xpcall(func, error_traceback, user_ob)
    end
    --]]

    ---! 通知玩家离开游戏
    local result = {}
    result.reason = reason or 0
    --USER_D:send_packet(clientAddr, clientId, "MSGS2CLeaveGame", result)

    ---! 断开客户端连接
    LOGIN_D:disconnect(user_ob)

    --退出匹配
    MATCH_D:quit_match(user_ob)
    --退出战斗
    COMBAT_D:quit_combat(user_ob)
    --退出队伍
    TEAM_D:quit_team(user_ob)

    ---! 记录重登日志
    ----todo:
end

---! 获取所有在线玩家
function USER_D:get_all_users()
    return user_map
end

---! 获取所有临时玩家
function USER_D:get_all_temp_users()
    return temp_map
end

---! 获取所有加载玩家
function USER_D:get_all_load_users()
    return load_map
end

---! 检查是否是好友
function USER_D:check_is_friend(user_ob,other_ob)
    return true
end

---! 获取用户基本信息 主界面显示的内容
function USER_D:get_base_info(user_ob)
    local base = {}
    if user_ob then
        base.user_id = user_ob:get_id()
        base.nick_name = user_ob:get_nick_name()
        base.level = user_ob:get_level()
        base.level_exp = CONFIG_D:get_exp_by_level(base.level); -- 当前等级升级总经验
        base.exp = user_ob:get_exp()
        base.avatar_url = user_ob:get_avatar_url()
        base.tank_id = user_ob:get_tank_id()
    else 
        base.user_id = 0
        base.nick_name = ""
        base.level = 0
        base.level_exp = 0
        base.exp = 0
        base.avatar_url = ""
        base.tank_id = 0
    end
    return base
end

---! 发送玩家消息
function USER_D:send_user_info(user_ob)
    local result = USER_D:get_base_info(user_ob)
    user_ob:send_packet("tanker.S2CUserInfo", result)
end

---! 发包
function USER_D:send_packet(gate_channel, agent_id, packet_name, packet_data)
    local ok, bytes = PROTOCOL_D:encode(packet_name, packet_data)
    if not ok then
        return
    end

    local data = {}
    data.agent_id = agent_id
    data.timestamp = os.time()
    data.bytes = bytes
    local ok, buffer = PROTOCOL_D:encode("tunnel.forward", data)
    if not ok then
        return
    end

    SERVICE_D:post("client_channel", gate_channel, buffer)
end

---! 广播
function USER_D:brocast_packet(user_obs, packet_name, packet_data, exclude_user_ids)
    if #user_obs <= 0 then
        return
    end

    local ok, bytes = PROTOCOL_D:encode(packet_name, packet_data)
    if not ok then
        return
    end

    local now_time = os.time()
    for _, user_ob in ipairs(user_obs) do repeat
        local agent = user_ob:get_agent()
        if not agent then
            break
        end

        local data = {}
        data.agent_id = agent:get_id()
        data.timestamp = now_time
        data.bytes = bytes
        local ok, buffer = PROTOCOL_D:encode("tunnel.forward", data)
        if not ok then
            break
        end

        SERVICE_D:post("client_channel", agent:get_gate_channel(), buffer)
    until true end
end

---! 根据排名增加经验
function USER_D:add_exp_by_top(user_ob, top)
    local exp = USER_D:get_exp_by_top(top)
    -- 给玩家增加经验
    USER_D:add_exp(user_ob,exp)
end

local exp_by_top_conf = {15,12,10,8,5,2} -- 根据排名获取经验的配置数据
---！ 根据排名获取经验
function USER_D:get_exp_by_top(top)
    local exp = 5 -- 基础经验
    if top >= 1 and top <= 4 then
        exp = exp + exp_by_top_conf[top]
    elseif 5 == top or 6 == top then
        exp = exp + exp_by_top_conf[5]
    elseif 7 == top or 8 == top then
        exp = exp + exp_by_top_conf[6]
    end
    return exp
end

-- 根据经验升级 返回 新的等级，剩余经验
function USER_D:level_up(level,exp)
    local need_exp = CONFIG_D:get_exp_by_level(level) --需要的经验
    local new_level = level
    while need_exp > 0 and exp >= need_exp do
        new_level = new_level + 1
        exp = exp - need_exp
        need_exp = CONFIG_D:get_exp_by_level(new_level)
    end
    return new_level, exp
end

---! 给玩家增加经验
function USER_D:add_exp(user_ob,exp)
    local all = user_ob:get_exp() + exp --总经验
    local level = user_ob:get_level() --当前等级
    level, all = USER_D:level_up(level, all)

    user_ob:set_exp(all)
    user_ob:set_level(level)
    return true
end

---！客户端获取用户信息
function USER_D:C2SGetUserDetailInfo(user_ob,msg)
    local u 
    --其他玩家信息
    if msg and msg.user_id then
        u = self:find_user(msg.user_id)
        if not u then
            u = self:load_user(msg.user_id)
        end
    else
        u = user_ob
    end

    local result = {}
    local base_info = USER_D:get_base_info(u) -- 基本信息
    result.info = base_info
    if u then
        result.steam_id = u:get_steam_id()
        result.victory = u:get_victory()
        result.victory_rate = u:get_victory_rate()
        result.tank_num = u:get_tank_num()
    else
        result.steam_id = ""
        result.victory = 0
        result.victory_rate = 0
        result.tank_num = 0
    end
    user_ob:send_packet("tanker.S2CGetUserDetailInfo", result)
end

---! 大厅界面更换坦克
function USER_D:C2SChangeTank(user_ob, msg)
    local result = {}
    --战斗中不能更换
    if user_ob:is_in_combat() then
        result.tank_id = 0
    else
        local tank = CONFIG_D:get_tank(msg.tank_id)
        if not tank then
            result.tank_id = 0
        else
            user_ob:set_tank_id(msg.tank_id)
            result.tank_id = msg.tank_id
        end
    end
    
    user_ob:send_packet("tanker.S2CChangeTank", result)
end

---! 注册进入游戏时的事件处理
function USER_D:register_enter_world(event_name, event_func)
    enter_world_map[event_name] = event_func
end

---! 注册离开游戏时的事件处理
function USER_D:register_leave_world(event_name, event_func)
    leave_world_map[event_name] = event_func
end

-------------------------------------------------------------------------------
---! 启动接口
-------------------------------------------------------------------------------
register_post_init(function()
    TIMER_D:start_timer(30, _check_user)

    ---! 仅为于便于观察服务器在线人数相关状态
    TIMER_D:start_timer(300, function()
        spdlog.info("user", string.format("user_map size : %d, temp_map size : %d, load_map size : %d, used_mem size : %s",
                                           table.size(user_map), table.size(temp_map), table.size(load_map), collectgarbage("count")))

        ---! 在线日志
        ----todo:
    end)
end)

register_post_dest(function()
    ---! 遍历在线玩家
    for _, user_ob in pairs(user_map) do
        local ok = pcall(function()
            ---! 断开玩家连接
            USER_D:disconnect(user_ob, 4)

            ---! 安排玩家下线
            USER_D:leave_world(user_ob)
        end)

        if not ok then
            -- spdlog.error("user", string.format("save user data failed, user = %s, data = %s", user_ob:get_id(), serialize(user_ob:query_entire_dbase())))
        end
    end
end)
